using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Timers;
using System.Web.Caching;
using Destra.Common.Caching.Queries;

namespace Destra.Common.Caching
{
    /// <summary>
    /// Implements a specific DacCacheManager to be used in DACGenerated classes.
    /// </summary>
    public class DacCacheManager : CacheBase
    {
        private static readonly DacCacheManager _instance = new DacCacheManager();
        private Dictionary<string, int> changedTables;
        private System.Timers.Timer timer;
        private FileSystemWatcher watcher;

        /// <summary>
        /// DacCacheManager constructor that creates an instance of a file system watcher to raise
        /// events when new text files (*.txt) are created in the <see cref="CacheBase.CachePath"/>.
        /// </summary>
        public DacCacheManager()
        {
            InitializeCacheDependencyFlusher();
        }

        /// <summary>
        /// Instance of the DacCacheManger class.
        /// </summary>
        public static DacCacheManager Instance
        {
            get { return _instance; }
        }

        private void InitializeCacheDependencyFlusher()
        {
            // Create a seperate thread that will poll the database(s) for changes to the underlying tables and call the Remove(changed tables) method.
            if (timer == null && !CacheConfiguration.Instance.DatabasePollSeconds.Equals(0))
            {
                // reset our changed tables dictionary
                changedTables = new Dictionary<string, int>();

                // restart our timer
                timer = new System.Timers.Timer(CacheConfiguration.Instance.DatabasePollSeconds*1000);
                timer.Elapsed += timer_Elapsed;
                timer.Disposed += timer_Disposed;
                timer.Start();
            }

            // setup the file system watcher
            if (watcher == null)
            {
                watcher = new FileSystemWatcher(CachePath, "*.txt");

                // only set the created event
                watcher.Created += watcher_Created;

                // start watching events
                watcher.IncludeSubdirectories = true;
                watcher.EnableRaisingEvents = true;
            }
        }

        /// <summary>
        /// Adds a object into the cache and sets a key based dependency on a string array of table names.
        /// </summary>
        /// <param name="key">The top level cache key to assign for this object.</param>
        /// <param name="value">The object to cache.</param>
        /// <param name="tableNames">An array of table names to cache. Note that the table names should be in the Schema.TableName format.</param>
        public void Add(string key, object value, params string[] tableNames)
        {
            // we have to have something in the cache already for these table names so 
            // lets add a simple bool value so that we can add/expire the cache.
            foreach (string dependentCacheKey in tableNames)
            {
                if (Cache[dependentCacheKey] == null)
                {
                    Cache.Insert(dependentCacheKey, true);
                }
            }

            // insert our object to cache
            Cache.Insert(key, value, new CacheDependency(null, tableNames));
        }

        /// <summary>
        /// Method to remove items from the cache.
        /// </summary>
        /// <param name="tableNames">The names of the underlying tables to expire the cache for.</param>
        public void Remove(params string[] tableNames)
        {
            // call our overloaded method with default parameters.
            Remove(true, tableNames);
        }

        /// <summary>
        /// Method to remove items from the cache.
        /// </summary>
        /// <param name="persist">If true a text file will be written to the path defined in the configuration.
        /// The path should be distributed by DSF in Windows 2003 R2. This combined with the FileSystemWatcher
        /// created when the class is created (in the singleton <see cref="Instance"/> method) creates a simple
        /// syndication mechanism. The persist is True by default.</param>
        /// <param name="tableNames">The names of the underlying tables to expire the cache for.</param>
        public void Remove(bool persist, params string[] tableNames)
        {
            // persist the remove file on the local file system (we will use DFS to transfer the file to all listeners).
            if (persist)
            {
                InitializeCacheDependencyFlusher();

                using (
                    TextWriter writer =
                        new StreamWriter(
                            Path.Combine(CachePath, "Cache-Remove-" + Guid.NewGuid() + ".txt")))
                {
                    writer.WriteLine(string.Format("Synchronization-Date: {0}", DateTime.Now));
                    writer.WriteLine(
                        string.Format("Synchronization-Server: {0}", Environment.MachineName.ToUpperInvariant()));
                    writer.WriteLine(string.Join(Environment.NewLine, tableNames));
                }
            }

            // remove from the cache, this will cascade and flush all child keys from the cache
            foreach (string tableName in tableNames)
            {
                Cache.Remove(tableName);
            }
        }

        #region Timer

        private void timer_Disposed(object sender, EventArgs e)
        {
            // make sure that we re-create our timer object
            InitializeCacheDependencyFlusher();
        }

        private void timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            // a temporary collection to hold a list of the tables that have a new changeId
            List<string> tablesNamesThatExpireCache = new List<string>();

            // check the stored information
            foreach (
                GetCacheTablesForChangeNotification.Entity entity in GetCacheTablesForChangeNotification.ReadEntities())
            {
                if (changedTables.ContainsKey(entity.tableName) && entity.changeId > changedTables[entity.tableName])
                {
                    tablesNamesThatExpireCache.Add(entity.tableName.ToUpperInvariant());

                    // remove the table from the expected range
                    changedTables.Remove(entity.tableName);
                }
                else if (!changedTables.ContainsKey(entity.tableName))
                {
                    // add to our changed tables dictionary
                    try
                    {
                        changedTables.Add(entity.tableName, entity.changeId);
                    }
                    catch (Exception ex)
                    {
                        if (ex.Message.Contains("An item with the same key has already been added."))
                        {
                            // bury this exception
                        }
                        else
                        {
                            throw;
                        }
                    }
                }
            }

            // flush anything that has changed
            if (!tablesNamesThatExpireCache.Count.Equals(0))
            {
                Remove(true, tablesNamesThatExpireCache.ToArray());
            }
        }

        #endregion

        #region File System Watcher

        /// <summary>
        /// Used to detect changes when data is removed from the cache. The NTFS will fire an
        /// event saying that a file has been created. This newly created file will contain all
        /// of the cache keys to invalidate. There will need to be another process that cleans
        /// up the expired files.
        /// </summary>
        private void watcher_Created(object sender, FileSystemEventArgs e)
        {
            string[] lines = ReadTextFile(e.FullPath, 0);
            foreach (string line in lines)
            {
                if (!string.IsNullOrEmpty(line))
                {
                    if (line.StartsWith("Synchronization-Date: "))
                    {
                        // do nothing here
                    }
                    else if (line.StartsWith("Synchronization-Server: "))
                    {
                        // we don't care about flushing the cache if the server that created the file is this one 
                        if (line.Substring(24).Trim().Equals(Environment.MachineName.ToUpperInvariant()))
                        {
                            break;
                        }
                    }
                    else
                    {
                        // remove the cache key
                        Instance.Remove(false, line);
                    }
                }
            }
        }

        #region Read Text File

        /// <summary>
        /// 
        /// </summary>
        private string[] ReadTextFile(string fullPath, int retryAttempt)
        {
            try
            {
                if (!File.Exists(fullPath) || retryAttempt >= 10)
                {
                    return new string[] {""};
                }

                return File.ReadAllLines(fullPath);
            }
            catch (IOException)
            {
                Thread.Sleep(1000);
                return ReadTextFile(fullPath, retryAttempt + 1);
            }
        }

        #endregion

        #endregion
    }
}